Winston vs Pino 回顾
| 维度 | Winston | Pino |
|---|---|---|
| 手动日志 | 每个接口手动添加 this.logger.error() | 开箱即用,自动记录请求 |
| 文件滚动 | winston-daily-rotate-file,功能丰富 | pino-roll,够用但不如 Winston 灵活 |
| 适用场景 | 对日志格式和存储有精细控制需求 | 追求简洁、不想手动加日志 |
实际项目中根据团队偏好取舍即可。但无论用哪种库,逐接口加 try-catch 手动记录错误都太低效——我们需要一种机制自动捕获所有异常并记录日志。
NestJS 异常处理机制
NestJS 内置了全局异常处理层,框架会在请求管道末端自动捕获所有未处理的异常:
客户端请求 → Middleware → Guard → Interceptor → Controller → Service
↓ 异常
Exception Filter(全局/控制器/路由三级)
↓
统一格式化响应 → 返回客户端
text
异常过滤器分三个层级(作用范围由小到大):
| 层级 | 装饰位置 | 作用范围 |
|---|---|---|
| 路由过滤器 | 单个路由方法 | 仅该路由 |
| 控制器过滤器 | @Controller() 上 | 该控制器所有路由 |
| 全局过滤器 | app.useGlobalFilters() | 整个应用 |
内置 HTTP Exception
NestJS 提供了一系列语义化的异常类,全部从 @nestjs/common 导入:
import {
HttpException, HttpStatus,
NotFoundException,
ForbiddenException,
UnauthorizedException,
BadRequestException,
InternalServerErrorException,
} from '@nestjs/common';
typescript
使用方式:
// 通用方式
throw new HttpException('User is not admin', HttpStatus.FORBIDDEN);
// → { statusCode: 403, message: "User is not admin" }
// 语义化快捷方式
throw new ForbiddenException('User is not admin');
// → { statusCode: 403, message: "User is not admin" }
throw new NotFoundException('User not found');
// → { statusCode: 404, message: "User not found" }
throw new UnauthorizedException('Invalid credentials');
// → { statusCode: 401, message: "Invalid credentials" }
typescript
常见状态码对应的异常类:BadRequestException (400)、UnauthorizedException (401)、ForbiddenException (403)、NotFoundException (404)、ConflictException (409)、InternalServerErrorException (500)。
自定义全局异常过滤器
创建 filters/http-exception.filter.ts:
import {
ExceptionFilter, Catch, ArgumentsHost,
HttpException, HttpStatus, LoggerService,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
constructor(private readonly logger: LoggerService) {}
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
// 记录错误日志到文件
this.logger.error(
`${request.method} ${request.url} → ${status}: ${exception.message}`,
exception.stack,
);
// 返回友好的 JSON 响应
response.status(status).json({
code: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
message: exception.message || exception.name,
});
}
}
typescript
参数说明
@Catch(HttpException)— 指定捕获的异常类型。不传参数则捕获所有异常host.switchToHttp()— 获取 HTTP 上下文,从中提取request和responseexception.getStatus()— 获取 HTTP 状态码exception.stack— 获取堆栈信息,用于日志排查
捕获所有异常(兜底过滤器)
上面的 HttpExceptionFilter 只捕获 HTTP 异常。对于未预期的运行时错误(如数据库连接断开、内存溢出),需要一个兜底过滤器:
import {
ExceptionFilter, Catch, ArgumentsHost,
HttpException, HttpStatus, LoggerService,
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
constructor(
private readonly logger: LoggerService,
private readonly httpAdapterHost: HttpAdapterHost,
) {}
catch(exception: unknown, host: ArgumentsHost): void {
const { httpAdapter } = this.httpAdapterHost;
const ctx = host.switchToHttp();
const httpStatus =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const responseBody = {
code: httpStatus,
timestamp: new Date().toISOString(),
path: httpAdapter.getRequestUrl(ctx.getRequest()),
message: exception instanceof Error ? exception.message : 'Internal server error',
};
this.logger.error(
`${httpAdapter.getRequestUrl(ctx.getRequest())} → ${httpStatus}`,
exception instanceof Error ? exception.stack : undefined,
);
httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
}
}
typescript
HttpAdapterHost 是平台无关的写法,兼容 Express 和 Fastify。
全局注册
在 main.ts 中注册:
// main.ts
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';
const app = await NestFactory.create(AppModule);
const logger = app.get(LoggerService);
const httpAdapterHost = app.get(HttpAdapterHost);
// 兜底过滤器在前,HTTP 过滤器在后
app.useGlobalFilters(
new AllExceptionsFilter(logger, httpAdapterHost),
new HttpExceptionFilter(logger),
);
typescript
全局过滤器只能有一个
@Catch()无参的兜底过滤器,但可以注册多个特定类型的过滤器。
过滤器与日志联动
注册后,所有未处理异常自动被过滤器捕获并记录到 Winston 日志文件中。无需在每个 Controller 方法中手动 try-catch:
// 之前:每个方法都要加 try-catch
@Get()
findAll() {
try {
return this.userService.findAll();
} catch (err) {
this.logger.error(err.message, err.stack);
throw err;
}
}
// 之后:直接抛异常,全局过滤器自动记录
@Get()
findAll() {
const users = this.userService.findAll();
if (!users.length) throw new NotFoundException('No users found');
return users;
}
typescript
补充:WebSocket 和微服务异常
NestJS 还为不同传输层提供了专用异常过滤器基类:
- WebSocket: 继承
BaseWsExceptionFilter - 微服务 (gRPC/RMQ): 继承
BaseRpcExceptionFilter
这些过滤器的使用方式与 HTTP 过滤器一致,区别在于 host.switchToWs() 或 host.switchToRpc() 获取对应上下文。
↑